/* =========================================================================
 * Copyright (c) 2006 and onwards, Josh Glover <jmglov@jmglov.net>,
 * Robert Ota Dieterich <otanobunaga@gmail.com>
 *
 * LICENCE:
 *
 *   This file is distributed under the terms of the BSD-2 License.
 *   See the COPYING file, which should have been distributed with
 *   this file, for details. If you did not receive the COPYING file,
 *   see:
 *
 *   http://www.jmglov.net/opensource/licenses/bsd.txt
 */

// C++ standard library headers
#include <iostream>
#include <string>
#include <vector>

// C standard library headers
#include <stdlib.h>
#include <time.h>

// SDL headers
#include "SDL.h"

// TMC headers
#include "City.h"
#include "MissileLauncher.h"
#include "Missile.h"

using namespace std;

// Constants
static const string NAME = "Tokyo Missile Command";

class Game {

private:

  // Constants
  static const int WINDOW_WIDTH  = 800;
  static const int WINDOW_HEIGHT = 600;

  static const Uint32 FRAMES_PER_SECOND = 20;

  static const Uint8 BG_COLOUR_R = 0;
  static const Uint8 BG_COLOUR_G = 0;
  static const Uint8 BG_COLOUR_B = 0;

  static const Uint8 FG_COLOUR_R = 255;
  static const Uint8 FG_COLOUR_G = 255;
  static const Uint8 FG_COLOUR_B = 255;

  static const Uint16 CITY_MAX_NUM   = 4;
  static const Uint16 CITY_START_NUM = CITY_MAX_NUM;

  static const Sint16 CITY_X_INTERVAL = ((WINDOW_WIDTH - City::WIDTH) /
                                         (CITY_MAX_NUM + 1));
  static const Sint16 CITY_Y_INTERVAL = 0;
  static const Sint16 CITY_X_START    = CITY_X_INTERVAL;
  static const Sint16 CITY_Y_START    = WINDOW_HEIGHT - City::HEIGHT;

  static const Uint16 LAUNCHER_MAX_NUM   = 3;
  static const Uint16 LAUNCHER_START_NUM = LAUNCHER_MAX_NUM;

  static const Sint16 LAUNCHER_X_INTERVAL = ((WINDOW_WIDTH -
                                              MissileLauncher::WIDTH) /
                                             (LAUNCHER_MAX_NUM - 1));
  static const Sint16 LAUNCHER_Y_INTERVAL = 0;
  static const Sint16 LAUNCHER_X_START    = 0;
  static const Sint16 LAUNCHER_Y_START    = (WINDOW_HEIGHT -
                                             MissileLauncher::HEIGHT);

  static const Uint32 LEVEL_START = 1;
  static const Uint32 SCORE_START = 0;
  static const Uint32 KILLS_START = 0;

  static const Uint32 MISSILE_INTERVAL_START = 5000; // 5 seconds
  static const Sint16 MISSILE_SPEED_START    = 100;

  SDL_Surface* m_pSurface; // main screen

  vector<ObjBase*>         m_vObjects;   // all objects on the screen
  vector<MissileLauncher*> m_vLaunchers; // all missile bases
  vector<City*>            m_vCities;    // all cities
  vector<Missile*>         m_vMissiles;  // all missiles
  vector<Sprite*>          m_vTargets;   // all targets

  bool m_isOver; // is the game over?

  Uint32 m_lastTick; // tick count of last iteration of think()

  Uint32 m_bgColour; // surface-mapped background colour
  Uint32 m_fgColour; // surface-mapped foreground colour

  Uint32 m_level; // current level
  Uint32 m_score; // current score
  Uint32 m_kills; // current number of incoming missiles shot down

  Uint32 m_lastMissileFired; // time at which last incoming missile was fired

  Uint32 m_missileInterval; // interval between new incoming missiles
  Sint16 m_missileSpeed;    // speed of incoming missiles

  void setWindowTitle(const string& str);
  void initGameData(void);

  bool handleInput(void);
  bool think(const Uint32 elapsedTicks);
  bool render(void);

  bool gameOver(void);

  Missile* newIncomingMissile(void);
  Missile* fireMissile(const Uint16 tx, const Uint16 ty);

  bool deleteObjects(void);

public:

  Game() {
    m_pSurface = NULL;

    m_isOver = false;

    m_lastTick = 0;

    m_bgColour = 0;
    m_fgColour = 0;

    m_level = LEVEL_START;
    m_score = SCORE_START;
    m_kills = KILLS_START;

    // Fire a missile at the begining of the game
    m_lastMissileFired = MISSILE_INTERVAL_START;

    m_missileInterval = MISSILE_INTERVAL_START;
    m_missileSpeed    = MISSILE_SPEED_START;
  } // Game()

  // Use the default C++ copy constructor
  // Game(const Game& rhs);

  // Use the default C++ destructor
  // ~Game();

  // Use the default C++ assignment operator
  // Game& operator= (const Game& rhs);

  void init(void);
  bool play(void);
  void cleanUp(void);

  inline bool isOver(void) { return m_isOver; }

}; // Game{}


int main(void) {
  Game game;

  game.init();

  // Main loop
  while(!game.isOver()) {
    if(!game.play()) {
      break;
    } // if (game over)
  } // while (main loop)

  game.cleanUp();

  return 0;
} // main()


void Game::init(void) {
  atexit(SDL_Quit);

  setWindowTitle("Initialising...");

  // Initialise SDL's video subsystem
  if (SDL_Init(SDL_INIT_VIDEO) < 0) {
    cerr << "Unable to init SDL: " << SDL_GetError() << endl;
    exit(1);
  } // if (SDL_Init() failed)

  // Create a window of the proper size
  if ((m_pSurface =
       SDL_SetVideoMode(WINDOW_WIDTH, WINDOW_HEIGHT, 0, SDL_SWSURFACE))
      == NULL) {
    cerr << "Unable to init video: " << SDL_GetError() << endl;
    exit(1);
  } // if (SDL_SetVideoMode() failed)

  // Map background and foreground colours to our surface
  m_bgColour = SDL_MapRGB(m_pSurface->format,
                          BG_COLOUR_R, BG_COLOUR_G, BG_COLOUR_B);
  m_fgColour = SDL_MapRGB(m_pSurface->format,
                          FG_COLOUR_R, FG_COLOUR_G, FG_COLOUR_B);

  // Initialise random number generator with the current Unix epoch time
  srandom((unsigned int)time(NULL));

  initGameData();

  setWindowTitle("");

  // Initialise the tick counter
  m_lastTick = SDL_GetTicks();
} // Game::init()

void Game::cleanUp(void) {
  setWindowTitle("Quitting...");

  // Delete all of our objects
  for (unsigned int i = 0; i < m_vObjects.size(); i++) {
    delete m_vObjects[i];
  } // for (deleting each object)

  SDL_Quit();
} // Game::cleanUp()

void Game::setWindowTitle(const string& str) {
  string window_title = NAME + " " + str;

  SDL_WM_SetCaption(window_title.c_str(), NULL);
} // Game::setWindowTitle()

void Game::initGameData(void) {
  Sint16 x, y;

  // Build cities (stairstep pattern)
  City* pCity = NULL;
  x = CITY_X_START;
  y = CITY_Y_START;
  for (int i = 0; i < CITY_START_NUM; i++) {
    pCity = new City(x, y, FG_COLOUR_R, FG_COLOUR_G, FG_COLOUR_B, m_pSurface);

    m_vCities.push_back(pCity);

    x += CITY_X_INTERVAL;
    y += CITY_Y_INTERVAL;
  } // for (building cities)

  // Add all cities to the objects and targets vectors as well
  for (unsigned int i = 0; i < m_vCities.size(); i++) {
    m_vObjects.push_back(m_vCities[i]);
    m_vTargets.push_back(m_vCities[i]);
  } // for (adding all cities to objects vector)

  // Build launchers (stairstep pattern)
  MissileLauncher* pLauncher = NULL;
  x = LAUNCHER_X_START;
  y = LAUNCHER_Y_START;
  for (int i = 0; i < LAUNCHER_START_NUM; i++) {
    pLauncher = new MissileLauncher(x, y, FG_COLOUR_R, FG_COLOUR_G, FG_COLOUR_B,
                                    m_pSurface);

    m_vLaunchers.push_back(pLauncher);

    x += LAUNCHER_X_INTERVAL;
    y += LAUNCHER_Y_INTERVAL;
  } // for (building cities)

  // Add all launchers to the objects and targets vectors as well
  for (unsigned int i = 0; i < m_vLaunchers.size(); i++) {
    m_vObjects.push_back(m_vLaunchers[i]);
    m_vTargets.push_back(m_vLaunchers[i]);
  } // for (adding all launchers to objects vector)
} // Game::initGameData()

bool Game::play(void) {
  Uint32 elapsedTicks = SDL_GetTicks() - m_lastTick;
  m_lastTick          = SDL_GetTicks();

  if (!handleInput())       return false;
  if (!think(elapsedTicks)) return false;
  if (!render())            return false;

  SDL_Delay(1000 / FRAMES_PER_SECOND);

  return true;
} // Game::play()

bool Game::handleInput(void) {
  SDL_Event event;
  Missile* pMissile = NULL;

  while (SDL_PollEvent(&event)) {
    switch (event.type) {
      // Key is pressed
      case SDL_KEYDOWN:
        // If escape is pressed, quit
        if (event.key.keysym.sym == SDLK_ESCAPE) {
          m_isOver = true;
          return false;
        } // if (escape)
        break;

      // Key is released
      case SDL_KEYUP:
        break;

      // Mouse button is pressed
      case SDL_MOUSEBUTTONDOWN:
        pMissile = fireMissile(event.motion.x, event.motion.y);
        if (pMissile != NULL) {
          m_vMissiles.push_back(pMissile);
          m_vObjects.push_back(pMissile);
        } // if (we were able to fire a missile)
        break;

      // Mouse button is released
      case SDL_MOUSEBUTTONUP:
        break;

      // Window closed
      case SDL_QUIT:
        m_isOver = true;
        return false;
        break;
    } // switch (what type is the event?)
  } // while (polling for events)

  return true;
} // Game::handleInput()

bool Game::think(const Uint32 elapsedTicks) {
  // Check for the game over condition
  if (gameOver()) return false;

  // Move all missiles
  for (unsigned int i = 0; i < m_vMissiles.size(); i++) {
    m_vMissiles[i]->move();
  } // for (moving missiles)

  // If we have reached or exceeded the missile interval, fire a new one
  m_lastMissileFired += elapsedTicks;
  if (m_lastMissileFired >= m_missileInterval) {
    Missile* pMissile = newIncomingMissile();
    m_vMissiles.push_back(pMissile);
    m_vObjects.push_back(pMissile);

    m_lastMissileFired = 0;
  } // if (firing a new missile)

  // Delete all objects that have been destroyed
  if (!deleteObjects()) return false;

  return true;
} // Game::think()

bool Game::render(void) {
  // Lock the surface if necessary
  if (SDL_MUSTLOCK(m_pSurface)) {
    if (SDL_LockSurface(m_pSurface) == -1) {
      cerr << "Unable to lock surface: " << SDL_GetError() << endl;
      return false;
    } // if (SDL_LockSurface() failed)
  } // if (locking surface)

  // Fill the screen with the background colour
  if (SDL_FillRect(m_pSurface, NULL, m_bgColour) == -1) {
    return false;
  } // if (SDL_FillRect() failed)

  // Draw each object
  for (unsigned int i = 0; i < m_vObjects.size(); i++) {
    m_vObjects[i]->draw(m_pSurface);
  } // for (drawing each object)

  // Unlock the surface if necessary
  if (SDL_MUSTLOCK(m_pSurface)) {
    SDL_UnlockSurface(m_pSurface);
  } // if (unlocking surface)

  // Update the screen
  if (SDL_Flip(m_pSurface) == -1) {
    cerr << "Unable to flip buffers: " << SDL_GetError() << endl;
    return false;
  } // if (SDL_Flip() failed)

  return true;
} // Game::render()

bool Game::gameOver(void) {
  // If the targets vector is empty, the player has lost
  if (m_vTargets.size() == 0) {
    m_isOver = true;

    // Display game over animation: flicker 10 times a second for three seconds
    for (int i = 0; i < 30; i++) {
      // Lock the surface if necessary
      if (SDL_MUSTLOCK(m_pSurface)) {
        if (SDL_LockSurface(m_pSurface) == -1) {
          cerr << "Unable to lock surface: " << SDL_GetError() << endl;
          return true;
        } // if (SDL_LockSurface() failed)
      } // if (locking surface)

      if (SDL_FillRect(m_pSurface, NULL,
                       (i % 2 == 0 ? m_fgColour : m_bgColour)) == -1) {
        return true;
      } // if (SDL_FillRect() failed)

      // Unlock the surface if necessary
      if (SDL_MUSTLOCK(m_pSurface)) {
        SDL_UnlockSurface(m_pSurface);
      } // if (unlocking surface)

      // Update the screen
      if (SDL_Flip(m_pSurface) == -1) {
        cerr << "Unable to flip buffers: " << SDL_GetError() << endl;
        return true;
      } // if (SDL_Flip() failed)

      SDL_Delay(100);
    } // for (playing game over animation)

    return true;
  } // if (game over, man!)

  return false;
} // Game::gameOver()

Missile* Game::newIncomingMissile(void) {
  // Randomly select a target
  int t = random() % m_vTargets.size();
  Sprite* pTarget = m_vTargets[t];

  // Randomly select an x coordinate (y is always 0, the top of the screen)
  Sint16 x = random() % WINDOW_WIDTH;
  Sint16 y = 0;

  // Randomly select a target x and y in or near the target (we don't want
  // incoming missiles to have *perfect* precision!)
  Sint16 tx = (pTarget->getX() - (pTarget->getW() / 4) +
               random() % (pTarget->getW() * 3 / 2));
  Sint16 ty = pTarget->getY();

  // dx and dy are simply the difference between the missile's initial
  // position and the target's (we use absolute value so we don't have
  // to have special cases for the missile's position being greater or
  // lesser than the target's)
  Sint16 dx = abs(x - tx) * (x > tx ? -1 : 1);
  Sint16 dy = abs(y - ty) * (y > ty ? -1 : 1);

  Missile* pMissile = new Missile(x, y,
                                  FG_COLOUR_R, FG_COLOUR_G, FG_COLOUR_B,
                                  m_pSurface,
                                  dx, dy, m_missileSpeed,
                                  0, WINDOW_WIDTH, 0, WINDOW_HEIGHT);
  pMissile->setTarget(pTarget);

  return pMissile;
} // Game::newIncomingMissile()

Missile* Game::fireMissile(const Uint16 tx, const Uint16 ty) {
  // If we have no missile launchers left, bail
  if (m_vLaunchers.size() == 0) return (Missile *)NULL;

  // Find the closest missile launcher to the targeted point
  MissileLauncher* pLauncher = m_vLaunchers[0];
  for (unsigned int i = 1; i < m_vLaunchers.size(); i++) {
    if (abs(tx - m_vLaunchers[i]->getX()) < abs(tx - pLauncher->getX())) {
      pLauncher = m_vLaunchers[i];
    } // if (found a closer launcher)
  } // for (finding closest launcher)

  Sint16 x = pLauncher->getX();
  Sint16 y = pLauncher->getY();

  Sint16 dx = abs(x - tx) * (x > tx ? -1 : 1);
  Sint16 dy = abs(y - ty) * (y > ty ? -1 : 1);

  return new Missile(x, y,
                     FG_COLOUR_R, FG_COLOUR_G, FG_COLOUR_B, m_pSurface,
                     dx, dy, m_missileSpeed,
                     0, WINDOW_WIDTH, 0, WINDOW_HEIGHT);
} // Game::fireMissile()

bool Game::deleteObjects(void) {
  vector<Sprite*> vDelete; // objects to be deleted

  vector<ObjBase*>::iterator         iObject;
  vector<MissileLauncher*>::iterator iLauncher;
  vector<City*>::iterator            iCity;
  vector<Missile*>::iterator         iMissile;
  vector<Sprite*>::iterator          iTarget;

  // Scan the missiles vector for objects that have been destroyed
  for (unsigned int i = 0; i < m_vMissiles.size(); i++) {
    if (m_vMissiles[i]->isDestroyed()) {
      vDelete.push_back(m_vMissiles[i]);
    } // if (found a destroyed object)
  } // for (scanning missiles vector)

  // Scan the targets vector for objects that have been destroyed
  for (unsigned int i = 0; i < m_vTargets.size(); i++) {
    if (m_vTargets[i]->isDestroyed()) {
      vDelete.push_back(m_vTargets[i]);
    } // if (found a destroyed object)
  } // for (scanning targets vector)

  // Delete all destroyed objects from all vectors
  Sprite* pDelete = NULL;
  for (unsigned int i = 0; i < vDelete.size(); i++) {
    pDelete = vDelete[i];

    // Objects vector
    iObject = find(m_vObjects.begin(), m_vObjects.end(), pDelete);
    if (*iObject == pDelete) {
      m_vObjects.erase(iObject);
    } // if (erasing from objects vector)

    // Launchers vector
    iLauncher = find(m_vLaunchers.begin(), m_vLaunchers.end(), pDelete);
    if (*iLauncher == pDelete) {
      m_vLaunchers.erase(iLauncher);
    } // if (erasing from launchers vector)

    // Cities vector
    iCity = find(m_vCities.begin(), m_vCities.end(), pDelete);
    if (*iCity == pDelete) {
      m_vCities.erase(iCity);
    } // if (erasing from cities vector)

    // Missiles vector
    iMissile = find(m_vMissiles.begin(), m_vMissiles.end(), pDelete);
    if (*iMissile == pDelete) {
      m_vMissiles.erase(iMissile);
    } // if (erasing from missiles vector)

    // Targets vector
    iTarget = find(m_vTargets.begin(), m_vTargets.end(), pDelete);
    if (*iTarget == pDelete) {
      m_vTargets.erase(iTarget);
    } // if (erasing from targets vector)

    delete pDelete;
  } // for (deleting all objects from deletion vector)

  // Clear the deletion vector
  vDelete.clear();

  return true;
} // Game::deleteObjects()
